package com.yuyaogc.lowcode.engine.plugin.activerecord;

import com.yuyaogc.lowcode.engine.annotation.Route;
import com.yuyaogc.lowcode.engine.annotation.Table;
import com.yuyaogc.lowcode.engine.context.Context;
import com.yuyaogc.lowcode.engine.entity.EntityClass;
import com.yuyaogc.lowcode.engine.entity.EntityField;
import com.yuyaogc.lowcode.engine.exception.EngineException;
import com.yuyaogc.lowcode.engine.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.Date;


/**
 * ActiveRecord Db
 *
 * @param <T>
 */
public class Model<T> extends KvMap implements Serializable {
    private Logger log = LoggerFactory.getLogger(Model.class);
    private Object[] NULL_PARA_ARRAY = new Object[0];

    public Model() {
    }


    public static Context getContext(){
        return Context.getInstance();
    }

    public static Config _getConfig() {
        Context context = Context.getInstance();
        return context.getConfig();
    }

    public EntityClass _getEntityClass(){
        return getContext().getEntityClass();
    }


    protected Class<? extends Model> _getModelClass() {
        Class clazz = this.getClass();

        if (clazz.isAnnotationPresent(Table.class)) {
            //TODO app name?

            Table table = (Table) clazz.getAnnotation(Table.class);
            Context.getInstance().get(table.name());
        }

        return clazz;
    }


    @Route
    public long count(Criteria criteria) {
       return DbUtil.count(_getModelClass(),Context.getInstance().getConfig(),criteria);
    }



    @Route(displayName = "查一条")
    public <T extends Model> T selectOne(Criteria criteria) {
        List<T> result = this.search(criteria, 0, 1, null);
        return result.isEmpty() ? null : result.get(0);
    }



    public static <T extends Model> T  selectOne(Class<? extends Model> type, Criteria criteria) {
        List<T> result = DbUtil.search(type,_getConfig(), criteria, 0, 1, null);
        return result.isEmpty() ? null : result.get(0);
    }

    public static  <T extends Model> List<T> search(Class<? extends Model> type, Criteria criteria, Integer offset, Integer limit, String order) {
        return DbUtil.search(type,Context.getInstance().getConfig(), criteria, offset, limit, order);
    }


    @Route
    public <T extends Model> List<T> search(Criteria criteria, Integer offset, Integer limit, String order) {
        return DbUtil.search(_getModelClass(), _getConfig(), criteria, offset, limit, order);
    }

    @Route
    public <T extends Model> boolean create(T value) {
        return value.save();
    }
    @Route
    public <T extends Model> boolean updateById(T value) {
        return value.update();
    }

    @Route(displayName = "保存")
    public boolean save() {
        _getModelClass();
        Config config = _getConfig();
        EntityClass table = _getEntityClass();

        getContext().call("validate", this);

        StringBuilder sql = new StringBuilder();
        List<Object> paras = new ArrayList();
        this.set("id", IdWorker.getId());
        config.dialect.forModelSave(table, this, sql, paras);
        Connection conn = null;
        PreparedStatement pst = null;
        boolean var8;
        try {
            conn = config.getConnection();
            if (config.getSqlDialect().isOracle()) {
                pst = conn.prepareStatement(sql.toString(), table.getPrimaryKey());
            } else {
                pst = conn.prepareStatement(sql.toString(), 1);
            }
            config.dialect.fillStatement(pst, paras);
            int result = pst.executeUpdate();
            config.dialect.getModelGeneratedKey(this, pst, table);
            this.clearModifyFlag();
            var8 = result >= 1;
        } catch (Exception var12) {
            throw new EngineException(var12);
        } finally {
            config.close(pst, conn);
        }


        for (EntityField entityField : table.getFields()) {
            entityField.getDataType().save(this, entityField);
        }


        return var8;
    }


    @Route(displayName = "更新")
    public boolean update() {
        _getModelClass();
        Config config = _getConfig();
        EntityClass entityClass = _getEntityClass();
        Context.getInstance().call("validate", this);

        String[] pKeys = entityClass.getPrimaryKey();
        for (String pKey : pKeys) {
            Object id = this.get(pKey);
            if (id == null) {
                throw new EngineException("You can't update model without Primary Key, " + pKey + " can not be null.");
            }
        }

        StringBuilder sql = new StringBuilder();
        List<Object> paras = new ArrayList<Object>();
        config.dialect.forModelUpdate(entityClass, this, _getModifyFlag(), sql, paras);

        if (paras.size() <= 1) {    // 参数个数为 1 的情况表明只有主键，也无需更新
            return false;
        }

        // --------
        Connection conn = null;
        try {
            conn = config.getConnection();
            int result = DbUtil.update(config, conn, sql.toString(), paras);
            if (result >= 1) {
                clearModifyFlag();
                return true;
            }

            for (EntityField entityField : entityClass.getFields()) {
                entityField.getDataType().write(this, entityField);
            }


            return false;
        } catch (Exception e) {
            throw new EngineException(e);
        }
    }

    @Route
    public <T extends Model> boolean validate(T value) {
        _getModelClass();
        if (!Objects.isNull(value)) {
            for (EntityField entityField : _getEntityClass().getFields()) {
                entityField.getDataType().validate(entityField, value);
            }
        }
        return true;
    }


    @Route
    public boolean delete() {
        _getModelClass();
        EntityClass table = _getEntityClass();
        String[] pKeys = table.getPrimaryKey();
        if (pKeys.length == 1) {    // 优化：主键大概率只有一个
            Object id = get(pKeys[0]);
            if (id == null)
                throw new ActiveRecordException("Primary key " + pKeys[0] + " can not be null");
            return deleteById(table, id);
        }

        Object[] ids = new Object[pKeys.length];
        for (int i = 0; i < pKeys.length; i++) {
            ids[i] = get(pKeys[i]);
            if (ids[i] == null)
                throw new ActiveRecordException("Primary key " + pKeys[i] + " can not be null");
        }
        return deleteById(table, ids);
    }

    /**
     * Delete model by id.
     *
     * @param idValue the id value of the model
     * @return true if delete succeed otherwise false
     */
    @Route
    public boolean deleteById(Object idValue) {
        if (idValue == null) {
            throw new IllegalArgumentException("idValue can not be null");
        }
        return deleteById(_getEntityClass(), idValue);
    }




    @Route
    public void deleteByIds(Object... idValues) {
        EntityClass entityClass = _getEntityClass();
        if (idValues == null || idValues.length != entityClass.getPrimaryKey().length) {
            throw new IllegalArgumentException("Primary key nubmer must equals id value number and can not be null");
        }
        deleteById(entityClass, idValues);
    }
    @Route
    public boolean deleteById(EntityClass table, Object... idValues) {
        _getModelClass();
        Config config = this._getConfig();
        Connection conn = null;

        boolean var6;
        try {
            conn = config.getConnection();
            String sql = config.dialect.forModelDeleteById(table);
            var6 = DbUtil.update(config, conn, sql, idValues) >= 1;
        } catch (Exception var10) {
            throw new ActiveRecordException(var10);
        } finally {
            config.close(conn);
        }

        return var6;
    }


    protected List<T> find(Config config, Connection conn, String sql, Object... paras) throws Exception {
        _getModelClass();
        try (PreparedStatement pst = conn.prepareStatement(sql)) {
            config.dialect.fillStatement(pst, paras);
            ResultSet rs = pst.executeQuery();
            List<T> result = config.dialect.buildModelList(rs, _getModelClass());
            DbUtil.close(rs);
            return result;
        } finally {
            config.close(conn);
        }
    }

    protected List<T> find(Config config, String sql, Object... paras) {
        Connection conn = null;
        try {
            conn = config.getConnection();
            return find(config, conn, sql, paras);
        } catch (Exception e) {
            throw new EngineException(e);
        } finally {
            config.close(conn);
        }
    }

    @Route
    public List<T> find(String sql, Object... paras) {
        return find(_getConfig(), sql, paras);
    }

    /**
     * @see #find(String, Object...)
     */
    @Route
    public List<T> find(String sql) {
        return find(sql, NULL_PARA_ARRAY);
    }

    @Route
    public List<T> findAll() {
        _getModelClass();
        Config config = _getConfig();
        String sql = config.dialect.forFindAll(_getEntityClass().getTableName());
        return find(config, sql, NULL_PARA_ARRAY);
    }

    @Route
    public T findFirst(String sql, Object... paras) {
        List<T> result = find(sql, paras);
        return result.size() > 0 ? result.get(0) : null;
    }

    @Route
    public T findById(Object idValue) {
        return findByIdLoadColumns(new Object[]{idValue}, "*");
    }

    @Route
    public T findByIds(Object... idValues) {
        return findByIdLoadColumns(idValues, "*");
    }

    @Route
    public T findByIdLoadColumns(Object idValue, String columns) {
        return findByIdLoadColumns(new Object[]{idValue}, columns);
    }

    @Route
    public T findByIdLoadColumns(Object[] idValues, String columns) {
        _getModelClass();
        Config config = _getConfig();
        EntityClass table = _getEntityClass();
        if (table.getPrimaryKey().length != idValues.length) {
            throw new IllegalArgumentException("id values error, need " + table.getPrimaryKey().length + " id value");
        }
        String sql = config.dialect.forModelFindById(table, columns);
        List<T> result = find(config, sql, idValues);
        return result.size() > 0 ? result.get(0) : null;
    }



    public String getStr(String attr) {
        Object s = this.get(attr);
        return s != null ? s.toString() : null;
    }

    public Integer getInt(String attr) {
        return TypeKit.toInt(this.get(attr));
    }

    public Long getLong(String attr) {
        return TypeKit.toLong(this.get(attr));
    }

    public BigInteger getBigInteger(String attr) {
        Object n = this.get(attr);
        if (n instanceof BigInteger) {
            return (BigInteger) n;
        } else if (n instanceof BigDecimal) {
            return ((BigDecimal) n).toBigInteger();
        } else if (n instanceof Number) {
            return BigInteger.valueOf(((Number) n).longValue());
        } else {
            return n instanceof String ? new BigInteger((String) n) : (BigInteger) n;
        }
    }

    public Date getDate(String attr) {
        return TypeKit.toDate(this.get(attr));
    }

    public LocalDateTime getLocalDateTime(String attr) {
        return TypeKit.toLocalDateTime(this.get(attr));
    }

    public Time getTime(String attr) {
        return (Time) this.get(attr);
    }

    public Timestamp getTimestamp(String attr) {
        return (Timestamp) this.get(attr);
    }

    public Double getDouble(String attr) {
        return TypeKit.toDouble(this.get(attr));
    }

    public Float getFloat(String attr) {
        return TypeKit.toFloat(this.get(attr));
    }

    public Short getShort(String attr) {
        return TypeKit.toShort(this.get(attr));
    }

    public Byte getByte(String attr) {
        return TypeKit.toByte(this.get(attr));
    }

    public Boolean getBoolean(String attr) {
        return TypeKit.toBoolean(this.get(attr));
    }

    public BigDecimal getBigDecimal(String attr) {
        return TypeKit.toBigDecimal(this.get(attr));
    }

    public byte[] getBytes(String attr) {
        return (byte[]) ((byte[]) this.get(attr));
    }

    public Number getNumber(String attr) {
        return TypeKit.toNumber(this.get(attr));
    }
}

